import { Layout, LayoutItem } from "./types";

/**
 * Just like the setTransform method, but instead it will return a negative value of right.
 *
 * @param top
 * @param right
 * @param width
 * @param height
 * @returns {{transform: string, WebkitTransform: string, MozTransform: string, msTransform: string, OTransform: string, width: string, height: string, position: string}}
 */
export function setTransformRtl(top: number, right: number, width: number, height: number) {
    // Replace unitless items with px
    const translate = "translate3d(" + right * -1 + "px," + top + "px, 0)";
    return {
        transform: translate,
        WebkitTransform: translate,
        MozTransform: translate,
        msTransform: translate,
        OTransform: translate,
        width: width + "px",
        height: height + "px",
        position: 'absolute',
        backgroundColor: '#41B883'
    };
}

export function setTransform(top: number, left: number, width: number, height: number) {
    // Replace unitless items with px
    const translate = "translate3d(" + left + "px," + top + "px, 0)";
    return {
        transform: translate,
        WebkitTransform: translate,
        MozTransform: translate,
        msTransform: translate,
        OTransform: translate,
        width: width + "px",
        height: height + "px",
        position: 'absolute',
        backgroundColor: '#DF9264'// randomColor()
    };
}

// 生成半透明颜色随机
export function randomColor() {
    return '#' + (function (h) {
        return new Array(7 - h.length).join("0") + h
    })((Math.random() * 0xffffff + 0.5).toString(16).substr(-6));
}

/**
 * Validate a layout. Throws errors.
 * @param  {Array}  layout        Array of layout items.
 * @param  {String} [contextName] Context name for errors.
 * @throw  {Error}                Validation error.
 */
export function validateLayout(layout: any, contextName: string = "Layout"): void {
    const subProps = ['x', 'y', 'w', 'h'];
    let keyArr = [];
    // 1. 必须是数组对象
    if (!Array.isArray(layout)) throw new Error(contextName + " must be an array!");
    // 2. 遍历数组对象 -- 里面的 x, y, w, h 必须是数字
    for (let i = 0, len = layout.length; i < len; i++) {
        const item = layout[i];
        for (let j = 0; j < subProps.length; j++) {
            if (typeof item[subProps[j]] !== 'number') {
                throw new Error('VueGridLayout: ' + contextName + '[' + i + '].' + subProps[j] + ' must be a number!');
            }
        }

        if (item.i === undefined || item.i === null) {
            throw new Error('VueGridLayout: ' + contextName + '[' + i + '].i cannot be null!');
        }

        if (typeof item.i !== 'number' && typeof item.i !== 'string') {
            throw new Error('VueGridLayout: ' + contextName + '[' + i + '].i must be a string or number!');
        }

        if (keyArr.indexOf(item.i) >= 0) {
            throw new Error('VueGridLayout: ' + contextName + '[' + i + '].i must be unique!');
        }
        keyArr.push(item.i);

        if (item.static !== undefined && typeof item.static !== 'boolean') {
            throw new Error('VueGridLayout: ' + contextName + '[' + i + '].static must be a boolean!');
        }
    }
}


/**
 * 获取所有的静态元素
 * Get all static elements.
 * @param  {Array} layout Array of layout objects.
 * @return {Array}        Array of static layout items..
 */
export function getStatics(layout: Layout) {
    return layout.filter((l: LayoutItem) => l.static) as Layout;
}

/**
 * Get layout items sorted from top left to right and down.
 * 按照从左到右、从上到下的顺序对布局对象进行排序。
 * @return {Array} Array of layout objects.
 * @return {Array}        Layout, sorted static items first.
 */
export function sortLayoutItemsByRowCol(layout: Layout) {
    return [].concat(layout as []).sort(function (a: LayoutItem, b: LayoutItem) {
        // 如果起始坐标相同，则按照x排序
        if (a.y === b.y && a.x === b.x) {
            return 0;
        }
        // 如果起始坐标不同，则按照y排序 ，如果y相同，则按照x排序
        if (a.y > b.y || (a.y === b.y && a.x > b.x)) {
            return 1;
        }

        return -1;
    });
}


/**
 * Given two layoutitems, check if they collide.
 *
 * @return {Boolean}   True if colliding.
 */
export function collides(l1: LayoutItem, l2: LayoutItem): boolean {
    if (l1 === l2) return false; // same element
    if (l1.x + l1.w <= l2.x) return false; // l1 is left of l2
    if (l1.x >= l2.x + l2.w) return false; // l1 is right of l2
    if (l1.y + l1.h <= l2.y) return false; // l1 is above l2
    if (l1.y >= l2.y + l2.h) return false; // l1 is below l2
    return true; // boxes overlap
}


/**
 * Returns the first item this layout collides with.
 * It doesn't appear to matter which order we approach this from, although
 * perhaps that is the wrong thing to do.
 *
 * @param  {Object} layoutItem Layout item.
 * @return {Object|undefined}  A colliding layout item, or undefined.
 */
export function getFirstCollision(layout: Layout, layoutItem: LayoutItem) {
    for (let i = 0, len = layout.length; i < len; i++) {
        if (collides(layout[i], layoutItem))
            return layout[i];
    }
}

/**
* Compact an item in the layout.
* 对元素进行紧凑排序
* @param  {Array}  静态元素的位置
* @param  {Object} layoutItem Layout item.  要排序的元素对象
* @return {boolean} 是否对其进行紧凑排序
*/
export function compactItem(compareWith: Layout, item: LayoutItem, verticalCompact: boolean, minPositions: any) {
    if (verticalCompact) {
        // Move the element up as far as it can go without colliding.
        // 对 y 进行紧凑排序，没有元素可以进行上移动操作
        while (item.y > 0 && !getFirstCollision(compareWith, item)) {
            item.y--;
        }
    } else if (minPositions) {
        const minY = minPositions[item.i].y;
        while (item.y > minY && !getFirstCollision(compareWith, item)) {
            item.y--;
        }
    }

    // Move it down, and keep moving it down if it's colliding.
    let collides;
    while ((collides = getFirstCollision(compareWith, item))) {
        item.y = collides.y + collides.h;
    }
    return item;
}

/**
 * Given a layout, compact it. This involves going down each y coordinate and removing gaps
 * between items.
 * 给定一个布局，将其紧凑排列。具体操作是遍历每个y坐标，并移除项目之间的空隙。
 * @param  {Array} layout Layout. 布局数据
 * @param  {Boolean} verticalCompact Whether or not to compact the layout vertically. 决定是否紧凑布局
 * @param {Object} minPositions
 * @return {Array}       Compacted Layout.
 */
export function compact(layout: Layout, verticalCompact: boolean, minPositions: any = null) {
    // Statics go in the compareWith array right away so items flow around them.
    const compareWith = getStatics(layout);
    // We go through the items by row and column.
    const sorted = sortLayoutItemsByRowCol(layout);
    // Holding for new items.
    const out = Array(layout.length);

    for (let i = 0, len = sorted.length; i < len; i++) {
        let item: LayoutItem = sorted[i];

        // Don't move static elements
        if (!item.static) {
            // 不是静态元素，就对其进行紧凑排列
            item = compactItem(compareWith, item, verticalCompact, minPositions);

            // Add to comparison array. We only collide with items before this one.
            // Statics are already in this array.
            compareWith.push(item);
        }

        // Add to output array to make sure they still come out in the right order.
        out[layout.indexOf(item)] = item;

        // Clear moved flag, if it exists.
        item.moved = false;
    }

    return out;
}

/**
 * 获取布局中最大的y坐标
 * @param layout 布局
 * @returns 
 */
export function bottom(layout: Layout): number {
    let max = 0, bottomY;
    for (let i = 0, len = layout.length; i < len; i++) {
        bottomY = layout[i].y + layout[i].h;
        if (bottomY > max) max = bottomY;
    }
    return max;
}


/**
 * Get a layout item by ID. Used so we can override later on if necessary.
 *
 * @param  {Array}  layout Layout array.
 * @param  {String} id     ID
 * @return {LayoutItem}    Item at ID.
 */
export function getLayoutItem(layout: Layout, id: string) {
    for (let i = 0, len = layout.length; i < len; i++) {
        if (layout[i].i === id) return layout[i];
    }
}

export function getAllCollisions(layout: Layout, layoutItem: LayoutItem): Array<LayoutItem> {
    return layout.filter((l) => collides(l, layoutItem));
}


/**
 * Move an element. Responsible for doing cascading movements of other elements.
 *
 * @param  {Array}      layout Full layout to modify.
 * @param  {LayoutItem} l      element to move.
 * @param  {Number}     [x]    X position in grid units.
 * @param  {Number}     [y]    Y position in grid units.
 * @param  {Boolean}    [isUserAction] If true, designates that the item we're moving is
 *                                     being dragged/resized by th euser.
 */
export function moveElement(layout: Layout, l: LayoutItem, x: number | undefined, y: number, isUserAction: boolean, preventCollision: boolean = false): Layout {
    if (l.static) return layout;

    // Short-circuit if nothing to do.
    //if (l.y === y && l.x === x) return layout;

    const oldX = l.x;
    const oldY = l.y;

    const movingUp = y && l.y > y;
    // This is quite a bit faster than extending the object
    if (typeof x === 'number') l.x = x;
    if (typeof y === 'number') l.y = y;
    l.moved = true;

    // If this collides with anything, move it.
    // When doing this comparison, we have to sort the items we compare with
    // to ensure, in the case of multiple collisions, that we're getting the
    // nearest collision.
    let sorted = sortLayoutItemsByRowCol(layout);
    if (movingUp) sorted = sorted.reverse();
    const collisions = getAllCollisions(sorted, l);

    if (preventCollision && collisions.length) {
        l.x = oldX;
        l.y = oldY;
        l.moved = false;
        return layout;
    }

    // Move each item that collides away from this element.
    for (let i = 0, len = collisions.length; i < len; i++) {
        const collision = collisions[i];
        // console.log('resolving collision between', l.i, 'at', l.y, 'and', collision.i, 'at', collision.y);

        // Short circuit so we can't infinite loop
        if (collision.moved) continue;

        // This makes it feel a bit more precise by waiting to swap for just a bit when moving up.
        if (l.y > collision.y && l.y - collision.y > collision.h / 4) continue;

        // Don't move static items - we have to move *this* element away
        if (collision.static) {
            layout = moveElementAwayFromCollision(layout, collision, l, isUserAction);
        } else {
            layout = moveElementAwayFromCollision(layout, l, collision, isUserAction);
        }
    }

    return layout;
}


/**
* This is where the magic needs to happen - given a collision, move an element away from the collision.
* We attempt to move it up if there's room, otherwise it goes below.
*
* @param  {Array} layout            Full layout to modify.
* @param  {LayoutItem} collidesWith Layout item we're colliding with.
* @param  {LayoutItem} itemToMove   Layout item we're moving.
* @param  {Boolean} [isUserAction]  If true, designates that the item we're moving is being dragged/resized
*                                   by the user.
*/
export function moveElementAwayFromCollision(layout: Layout, collidesWith: LayoutItem,
    itemToMove: LayoutItem, isUserAction: boolean | null | undefined): Layout {
    const preventCollision = false // we're already colliding
    // If there is enough space above the collision to put this element, move it there.
    // We only do this on the main collision as this can get funky in cascades and cause
    // unwanted swapping behavior.
    if (isUserAction) {
        // Make a mock item so we don't modify the item here, only modify in moveElement.
        const fakeItem: LayoutItem = {
            x: itemToMove.x,
            y: itemToMove.y,
            w: itemToMove.w,
            h: itemToMove.h,
            i: '-1'
        };
        fakeItem.y = Math.max(collidesWith.y - itemToMove.h, 0);
        if (!getFirstCollision(layout, fakeItem)) {
            return moveElement(layout, itemToMove, undefined, fakeItem.y, preventCollision);
        }
    }

    // Previously this was optimized to move below the collision directly, but this can cause problems
    // with cascading moves, as an item may actually leapflog a collision and cause a reversal in order.
    return moveElement(layout, itemToMove, undefined, itemToMove.y + 1, preventCollision);
}
